<!doctype html><html lang="zh-CN"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>LN2真三轴&XCH₄-L2离线演示(优化版)</title>
<style>
:root{--blue:#1D4ED8;--deep:#0F2A4A;--lt:#E9F2FF;--gray:#7C8CA5;--border:#e5e7eb}
*{box-sizing:border-box}body{font-family:"Source Han Sans SC",system-ui,Arial;margin:0;background:#f7f9fc;color:#0B1320}
.wrap{max-width:1220px;margin:28px auto;padding:0 16px}
h1{margin:0 0 12px;font-size:28px;color:var(--deep)}
.tabs{display:flex;gap:8px;margin:8px 0 14px}
.tab{padding:10px 14px;border:1px solid var(--blue);border-radius:12px;color:var(--blue);cursor:pointer;background:#fff}
.tab.active{background:linear-gradient(0deg,var(--blue),#60A5FA);color:#fff;border:none}
.panel{display:none}.panel.show{display:block}
.card{background:#fff;border:1px solid var(--border);border-radius:16px;box-shadow:0 6px 18px rgba(15,42,74,.08);padding:12px;margin-bottom:12px}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
label{font-size:13px;color:#334155}
input[type="number"],input[type="text"],select{padding:6px 8px;border:1px solid #cbd5e1;border-radius:10px}
input[type="file"]{padding:6px}
button{padding:8px 12px;border:1px solid var(--blue);color:var(--blue);background:#fff;border-radius:10px;cursor:pointer}
button.primary{background:linear-gradient(0deg,var(--blue),#60A5FA);color:#fff;border:none}
.grid{display:grid;grid-template-columns:3fr 1fr;gap:12px}
.canvasWrap{background:#fff;border:1px solid var(--border);border-radius:16px;padding:10px;position:relative}
.canvas{width:100%;height:660px;border:1px solid var(--border);border-radius:12px;background:#fff}
.side{background:#fff;border:1px solid var(--border);border-radius:16px;padding:12px}
.kv{display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:13px}
.hr{height:1px;background:var(--border);margin:10px 0}
.note{font-size:12px;color:#475569}
footer{margin:16px 0 6px;font-size:12px;color:#334155}
.legend{position:absolute;right:16px;top:16px;background:#fff;border:1px solid var(--border);border-radius:10px;padding:8px}
.small{font-size:12px}
</style>

<div class="wrap">
  <h1>工程化数据处理工具—离线演示</h1>
  <div class="tabs">
    <div class="tab active" onclick="showTab('ln2',this)">LN2真三轴可视化</div>
    <div class="tab" onclick="showTab('xch4',this)">XCH₄-L2Processor</div>
  </div>

  <!-- LN2 -->
  <div id="panel-ln2" class="panel show">
    <div class="card">
      <div class="row">
        <label>PressureCSV</label><input id="pCsv" type="file" accept=".csv">
        <label>AECsv</label><input id="aCsv" type="file" accept=".csv">
        <label>TemperatureCSV</label><input id="tCsv" type="file" accept=".csv">
      </div>
      <div class="row" style="margin-top:8px">
        <label>采样率Hz</label><input id="fs" type="number" step="0.1" value="10" style="width:90px">
        <label>平滑窗口s</label><input id="smooth" type="number" step="1" value="5" style="width:90px">
        <label>破裂邻域自动缩放±s</label><input id="zoomWin" type="number" step="1" value="30" style="width:90px">
        <label>组合视图偏移</label><input id="sep" type="number" step="0.05" value="0.15" style="width:90px">
        <label>规范化</label><select id="norm" onchange="drawLN2()"><option value="1" selected>是</option><option value="0">否</option></select>
        <button onclick="loadDemoLN2()">加载Demo</button>
        <button class="primary" onclick="analyzeLN2()">分析</button>
        <button onclick="exportLN2()">导出结果CSV</button>
        <button onclick="savePNG('ln2Canvas')">导出PNG</button>
      </div>
    </div>

    <div class="grid">
      <div class="canvasWrap">
        <canvas id="ln2Canvas" class="canvas"></canvas>
        <div class="legend small">视图:
          <select id="ln2View" onchange="drawLN2()">
            <option value="pressure">Pressure</option>
            <option value="damage">Damage</option>
            <option value="temp">Temperature</option>
            <option value="combined" selected>Combined</option>
          </select>
        </div>
      </div>
      <div class="side">
        <b>指标</b>
        <div id="ln2Stats" class="kv" style="margin-top:6px"></div>
        <div class="hr"></div>
        <b>CSV要求</b>
        <div class="note">两列time,value或单列value(用上方采样率建时间)。单位建议:压力MPa、温度℃、AE为幅度或计数。</div>
        <div class="hr"></div>
        <b>方法</b>
        <div class="note">时间对齐→平滑→一阶导峰定位破裂时刻→AE累积归一化得到Damage→组合视图用偏移避免曲线重叠。</div>
      </div>
    </div>
  </div>

  <!-- XCH4 -->
  <div id="panel-xch4" class="panel">
    <div class="card">
      <div class="row">
        <label>选择CSV(lon,lat,dxch4)</label><input id="dxFile" type="file" accept=".csv">
        <button onclick="loadXCSV()">加载CSV</button>
        <button onclick="loadDemoX()">加载Demo</button>
        <label>色阶vmin</label><input id="vmin" type="number" step="0.1" value="0" style="width:80px">
        <label>vmax</label><input id="vmax" type="number" step="0.1" value="30" style="width:80px">
        <label>分位Qmin/Qmax%</label><input id="qmin" type="number" step="0.1" value="5" style="width:70px">–
        <input id="qmax" type="number" step="0.1" value="99.5" style="width:70px">
        <label>γ对比度</label><input id="gamma" type="number" step="0.1" value="1.0" style="width:70px">
        <label>像元px</label><input id="px" type="number" step="1" value="6" style="width:60px">
        <button onclick="applyScale()">应用色阶</button>
        <button class="primary" onclick="savePNG('xCanvas')">导出PNG</button>
        <button onclick="exportX()">导出结果CSV</button>
      </div>
    </div>

    <div class="grid">
      <div class="canvasWrap">
        <canvas id="xCanvas" class="canvas"></canvas>
        <div class="legend">
          <canvas id="legend" width="18" height="240"></canvas>
          <div style="text-align:center" class="small">ΔXCH₄(ppb)</div>
        </div>
      </div>
      <div class="side">
        <b>统计</b>
        <div id="xStats" class="kv" style="margin-top:6px"></div>
        <div class="hr"></div>
        <b>说明</b>
        <div class="note">本页只做结果可视化。建议在脚本中先做质量旗标/掩膜与月背景(P20/P30)后输出lon,lat,dxch4。</div>
      </div>
    </div>
  </div>

  <footer>版本留痕:view=2025-08-v3。离线运行,不含任何企业机密模型。</footer>
</div>

<script>
/* 通用 */
function showTab(key,btn){
  document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
  btn.classList.add('active');
  document.querySelectorAll('.panel').forEach(p=>p.classList.remove('show'));
  document.getElementById('panel-'+key).classList.add('show');
}
function savePNG(id){
  const a=document.createElement('a');
  a.href=document.getElementById(id).toDataURL('image/png');
  a.download=id+'.png'; a.click();
}
function parseCSVText(text){
  const lines=text.trim().split(/\r?\n/);
  const cols=lines[0].split(',').map(s=>s.trim());
  const rows=lines.slice(1).map(l=>l.split(',').map(s=>s.trim()));
  return {cols,rows};
}
function fitCanvas(c){
  const w=document.querySelector('.canvasWrap').clientWidth-22;
  c.width=w; c.height=660;
}

/* LN2 */
let LN2={t:[],p:[],ae:[],temp:[],fs:10,ruptureT:null,stats:{},viewWindow:null,proc:null};
const ln2C=document.getElementById('ln2Canvas'), lctx=ln2C.getContext('2d');
window.addEventListener('resize',()=>{fitCanvas(ln2C);drawLN2()}); fitCanvas(ln2C);

function loadDemoLN2(){
  const fs=Number(document.getElementById('fs').value||10);
  const T=600,n=T*fs; LN2.fs=fs;
  LN2.t=Array.from({length:n},(_,i)=>i/fs);
  LN2.p=LN2.t.map(t=>3+0.0025*t+(Math.sin(t*0.15)*0.08)+(t>300?(t-300)*0.01:0));
  LN2.p=LN2.p.map((v,i)=> i>420*fs? v+(i-420*fs)*0.03/fs : v);
  LN2.ae=LN2.t.map(t=> (Math.random()*0.3)+(t>410?(t-410)*0.05:0)+(t>420?(t-420)*0.4:0));
  LN2.temp=LN2.t.map(t=>20+0.002*t+(Math.sin(t*0.02)*0.5));
  analyzeLN2(true);
}
function readSeriesFromCSV(file,cb){
  const fs=Number(document.getElementById('fs').value||10);
  const r=new FileReader();
  r.onload=()=>{ const {cols,rows}=parseCSVText(r.result);
    let t=[],v=[];
    if(cols.length>=2 && ['time','t'].includes(cols[0].toLowerCase())){
      rows.forEach(a=>{ const tt=parseFloat(a[0]); const vv=parseFloat(a[1]); if(Number.isFinite(tt)&&Number.isFinite(vv)){t.push(tt);v.push(vv);} });
    }else{
      rows.forEach((a,i)=>{ const vv=parseFloat(a[0]); if(Number.isFinite(vv)){v.push(vv);} });
      t=Array.from({length:v.length},(_,i)=>i/fs);
    }
    cb({t,v,fs});
  };
  r.readAsText(file);
}
function analyzeLN2(fromDemo=false){
  const fs=Number(document.getElementById('fs').value||10); LN2.fs=fs;
  const pF=document.getElementById('pCsv').files[0], aF=document.getElementById('aCsv').files[0], tF=document.getElementById('tCsv').files[0];
  if(!fromDemo && (!pF||!aF||!tF)){ alert('请选择三份CSV'); return; }
  if(fromDemo){ finalizeLN2(); return; }
  readSeriesFromCSV(pF,pr=>{
    readSeriesFromCSV(aF,ar=>{
      readSeriesFromCSV(tF,tr=>{
        const n=Math.min(pr.v.length,ar.v.length,tr.v.length);
        LN2.t=Array.from({length:n},(_,i)=>i/fs);
        LN2.p=pr.v.slice(0,n); LN2.ae=ar.v.slice(0,n); LN2.temp=tr.v.slice(0,n);
        finalizeLN2();
      });
    });
  });
}
function finalizeLN2(){
  const fs=LN2.fs, winS=Number(document.getElementById('smooth').value||5);
  const win=Math.max(1,Math.round(winS*fs));
  const sm=a=>a.map((_,i)=>{let s=0,c=0; for(let k=i-win+1;k<=i;k++){ if(k>=0){s+=a[k];c++;}} return s/Math.max(1,c);});
  const dp=a=>a.map((v,i)=> i? (v-a[i-1])*fs : 0);
  const pS=sm(LN2.p), aeS=sm(LN2.ae), tS=sm(LN2.temp);
  const dP=dp(pS), dA=dp(aeS);
  let i1=dP.indexOf(Math.max(...dP)), i2=dA.indexOf(Math.max(...dA));
  const idx=Math.round((i1+i2)/2); LN2.ruptureT=LN2.t[idx];
  let acc=0; const aeAcc=aeS.map(v=>{ acc+=Math.max(0,v); return acc;});
  const maxAcc=Math.max(...aeAcc)||1; const damage=aeAcc.map(v=>v/maxAcc);
  LN2.proc={pS,aeS,tS,damage};
  const peakP=Math.max(...pS), meanT=avg(tS), stdT=std(tS);
  LN2.stats={峰值压力:peakP.toFixed(3),破裂时刻s:LN2.ruptureT.toFixed(1),AE累积终值:acc.toFixed(1),温度均值:meanT.toFixed(2),温度Std:stdT.toFixed(2)};
  drawLN2(); updateKV('ln2Stats',LN2.stats);
  const auto=Number(document.getElementById('zoomWin').value||0);
  LN2.viewWindow=auto>0?[Math.max(0,LN2.ruptureT-auto),Math.min(LN2.t.at(-1),LN2.ruptureT+auto)]:null;
}
function avg(a){ return a.reduce((x,y)=>x+y,0)/a.length }
function std(a){ const m=avg(a); return Math.sqrt(a.reduce((s,v)=>s+(v-m)*(v-m),0)/a.length) }

function drawLN2(){
  const view=document.getElementById('ln2View').value;
  const c=ln2C, ctx=lctx; ctx.clearRect(0,0,c.width,c.height);
  if(!LN2.t.length){ axes(ctx,c); return; }
  const [t0,t1]= LN2.viewWindow? LN2.viewWindow : [LN2.t[0], LN2.t.at(-1)];
  const i0=Math.max(0,LN2.t.findIndex(x=>x>=t0)), i1=Math.max(i0+2,LN2.t.findIndex(x=>x>=t1));
  const sl=a=>a.slice(i0, i1>0?i1:undefined);
  const t=sl(LN2.t), p=sl(LN2.proc.pS), dmg=sl(LN2.proc.damage), tp=sl(LN2.proc.tS);
  axes(ctx,c,'Time(s)', view==='pressure'?'Pressure':'' ,view);
  const plot=(arr,color,scale=1,off=0)=>{ path(ctx,t,arr.map(v=>off+v*scale),color); };
  if(view==='pressure'){ plot(p,'#1D4ED8'); markRupture(ctx,c); }
  else if(view==='damage'){ plot(dmg,'#EF4444'); markRupture(ctx,c); }
  else if(view==='temp'){ plot(tp,'#22C55E'); markRupture(ctx,c); }
  else{
    const norm=document.getElementById('norm').value==='1';
    const sep=parseFloat(document.getElementById('sep').value||0.15);
    const nrm=a=>{ if(!norm) return a; const mn=Math.min(...a),mx=Math.max(...a); return a.map(v=>(v-mn)/(mx-mn+1e-9)); };
    plot(nrm(p),'#1D4ED8',1,0);
    plot(nrm(dmg),'#EF4444',1,-sep);
    plot(nrm(tp),'#22C55E',1,+sep);
    markRupture(ctx,c);
    // 放到图内左上角的白底圆角徽章
    drawBadge(ctx, ctx._ax.x0+10, ctx._ax.y0+20,
      `蓝=Pressure${norm?'(归一)':''}，红=Damage${norm?'(归一)':''}(-${sep})，绿=Temperature${norm?'(归一)':''}(+${sep})`);
  }
}
function axes(ctx,c,xt='Time(s)',yt='',view=''){
  const x0=60,y0=28,x1=c.width-24,y1=c.height-44;
  ctx.strokeStyle='#94a3b8'; ctx.lineWidth=1; ctx.strokeRect(x0,y0,x1-x0,y1-y0);
  ctx.fillStyle='#334155'; ctx.font='12px system-ui'; ctx.fillText(xt,(x0+x1)/2-20,y1+28);
  // 网格
  for(let i=0;i<6;i++){ const x=x0+(x1-x0)*i/5; ctx.beginPath();ctx.moveTo(x,y0);ctx.lineTo(x,y1);ctx.strokeStyle='rgba(148,163,184,.35)';ctx.stroke(); }
  for(let i=0;i<6;i++){ const y=y0+(y1-y0)*i/5; ctx.beginPath();ctx.moveTo(x0,y);ctx.lineTo(x1,y);ctx.stroke(); }
  // 视图标签放右上角
  ctx.fillStyle='#0F2A4A'; ctx.font='14px system-ui'; ctx.fillText('视图:'+(view||''), x1-110, y0-8);
  ctx._ax={x0,y0,x1,y1};
}
function path(ctx,t,y,color){
  const {x0,y0,x1,y1}=ctx._ax; const n=t.length;
  const tx=i=> x0 + (x1-x0)*(t[i]-t[0])/(t.at(-1)-t[0]+1e-9);
  const mn=Math.min(...y), mx=Math.max(...y);
  const ty=v=> y1 - (y1-y0)*(v-mn)/(mx-mn+1e-9);
  ctx.beginPath(); ctx.strokeStyle=color; ctx.lineWidth=2;
  for(let i=0;i<n;i++){ const x=tx(i), yy=ty(y[i]); if(i===0) ctx.moveTo(x,yy); else ctx.lineTo(x,yy); }
  ctx.stroke();
}
function label(ctx,x,y,text){ ctx.fillStyle='#334155'; ctx.font='12px system-ui'; ctx.fillText(text,x,y); }
function roundRect(ctx,x,y,w,h,r){
  ctx.beginPath();
  ctx.moveTo(x+r,y);
  ctx.arcTo(x+w,y,x+w,y+h,r);
  ctx.arcTo(x+w,y+h,x,y+h,r);
  ctx.arcTo(x,y+h,x,y,r);
  ctx.arcTo(x,y,x+w,y,r);
  ctx.closePath();
}
function drawBadge(ctx,x,y,text){
  ctx.save(); ctx.font='12px system-ui';
  const padX=8,padY=4,r=8, w=ctx.measureText(text).width+padX*2, h=18;
  roundRect(ctx,x-padX,y-h+4,w,h,r);
  ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.fill();
  ctx.strokeStyle='rgba(15,42,74,.15)'; ctx.stroke();
  ctx.fillStyle='#334155'; ctx.fillText(text,x,y);
  ctx.restore();
}
function markRupture(ctx,c){
  if(LN2.ruptureT==null) return;
  const {x0,y0,y1,x1}=ctx._ax;
  const t0=LN2.viewWindow? LN2.viewWindow[0]:LN2.t[0];
  const t1=LN2.viewWindow? LN2.viewWindow[1]:LN2.t.at(-1);
  const x= x0 + (x1-x0)*(LN2.ruptureT-t0)/(t1-t0+1e-9);
  ctx.beginPath(); ctx.strokeStyle='#ef4444'; ctx.setLineDash([6,6]); ctx.moveTo(x,y0); ctx.lineTo(x,y1); ctx.stroke(); ctx.setLineDash([]);
  label(ctx,x+6,y0+14,'破裂≈'+LN2.ruptureT.toFixed(1)+'s');
}
function updateKV(id,obj){
  document.getElementById(id).innerHTML=Object.entries(obj).map(([k,v])=>`<div>${k}</div><div>${v}</div>`).join('');
}
function exportLN2(){
  if(!LN2.t.length){alert('请先分析');return;}
  const lines=['metric,value',
    'rupture_s,'+LN2.ruptureT.toFixed(3),
    'peak_pressure,'+LN2.stats.峰值压力,
    'ae_damage_final,'+LN2.stats.AE累积终值,
    'temp_mean,'+LN2.stats.温度均值,
    'temp_std,'+LN2.stats.温度Std,
    'fs,'+LN2.fs,
    'smooth_s,'+document.getElementById('smooth').value,
    'zoom_window_s,'+document.getElementById('zoomWin').value
  ].join('\n');
  const a=document.createElement('a'); a.href='data:text/csv;charset=utf-8,'+encodeURIComponent(lines); a.download='ln2_results.csv'; a.click();
}

/* XCH4 */
let X={pts:[],lonMin:0,lonMax:1,latMin:0,latMax:1,vmin:0,vmax:30};
const xC=document.getElementById('xCanvas'), xctx=xC.getContext('2d');
window.addEventListener('resize',()=>{fitCanvas(xC);drawX()}); fitCanvas(xC);

function parseX(text){
  const {cols,rows}=parseCSVText(text);
  const iL=cols.findIndex(c=>c.toLowerCase()==='lon'),
        iA=cols.findIndex(c=>c.toLowerCase()==='lat'),
        iV=cols.findIndex(c=>c.toLowerCase()==='dxch4');
  if(iL<0||iA<0||iV<0){alert('CSV需含列:lon,lat,dxch4');return [];}
  const out=[]; rows.forEach(r=>{
    const lo=parseFloat(r[iL]), la=parseFloat(r[iA]), v=parseFloat(r[iV]);
    if(Number.isFinite(lo)&&Number.isFinite(la)&&Number.isFinite(v)) out.push({lon:lo,lat:la,val:v});
  }); return out;
}
function loadXCSV(){
  const f=document.getElementById('dxFile').files[0]; if(!f){alert('请选择CSV');return;}
  const r=new FileReader(); r.onload=()=>{ X.pts=parseX(r.result); postX(); }; r.readAsText(f);
}
function loadDemoX(){
  X.pts=[]; for(let i=0;i<2500;i++){
    const lon=105+Math.random()*7, lat=33+Math.random()*7, base=Math.random()*4;
    const d1=(lon-109.2)**2+(lat-36.4)**2, d2=(lon-110.5)**2+(lat-38.1)**2;
    const bump=18*Math.exp(-d1/0.8)+12*Math.exp(-d2/0.6); X.pts.push({lon,lat,val:base+bump});
  } postX();
}
function postX(){
  if(!X.pts.length){alert('数据为空');return;}
  X.lonMin=Math.min(...X.pts.map(p=>p.lon)); X.lonMax=Math.max(...X.pts.map(p=>p.lon));
  X.latMin=Math.min(...X.pts.map(p=>p.lat)); X.latMax=Math.max(...X.pts.map(p=>p.lat));
  applyScale(true);
}
function quant(a,q){
  const s=[...a].sort((x,y)=>x-y); const i=(s.length-1)*q; const lo=Math.floor(i),hi=Math.ceil(i);
  return lo===hi? s[lo] : s[lo]*(hi-i)+s[hi]*(i-lo);
}
function applyScale(auto=false){
  const vals=X.pts.map(p=>p.val);
  if(auto){
    const qmin=parseFloat(document.getElementById('qmin').value)/100;
    const qmax=parseFloat(document.getElementById('qmax').value)/100;
    X.vmin=quant(vals,qmin); X.vmax=quant(vals,qmax);
    document.getElementById('vmin').value=X.vmin.toFixed(1);
    document.getElementById('vmax').value=X.vmax.toFixed(1);
  }else{
    X.vmin=parseFloat(document.getElementById('vmin').value)||0;
    X.vmax=parseFloat(document.getElementById('vmax').value)||30;
  }
  drawLegend(); drawX(); updateXStats();
}
function projX(lon){ return (lon-X.lonMin)/(X.lonMax-X.lonMin)*(xC.width-70)+50 }
function projY(lat){ return (1-(lat-X.latMin)/(X.latMax-X.latMin))*(xC.height-60)+30 }
function colorViridis(t){
  const a=[68,1,84], b=[59,82,139], c=[33,145,140], d=[94,201,98], e=[253,231,37];
  function mix(u,v,w){ return Math.round(u*(1-w)+v*w) }
  if(t<0) t=0; if(t>1) t=1;
  const s=t*3; if(s<1){ return [mix(a[0],b[0],s),mix(a[1],b[1],s),mix(a[2],b[2],s)]}
  else if(s<2){ const u=s-1; return [mix(b[0],c[0],u),mix(b[1],c[1],u),mix(b[2],c[2],u)]}
  else { const u=s-2; return [mix(c[0],d[0],u),mix(c[1],d[1],u),mix(c[2],d[2],u)]}
}
function drawLegend(){
  const leg=document.getElementById('legend'), ctx=leg.getContext('2d');
  const h=leg.height, w=leg.width, img=ctx.createImageData(w,h);
  for(let y=0;y<h;y++){ const t=1-y/(h-1); const [r,g,b]=colorViridis(t);
    for(let x=0;x<w;x++){ const k=(y*w+x)*4; img.data[k]=r; img.data[k+1]=g; img.data[k+2]=b; img.data[k+3]=255; } }
  ctx.putImageData(img,0,0);
}
function drawX(){
  const ctx=xctx, c=xC; ctx.clearRect(0,0,c.width,c.height);
  const x0=50,y0=30,x1=c.width-20,y1=c.height-40;
  ctx.strokeStyle='#94a3b8'; ctx.lineWidth=1; ctx.strokeRect(x0,y0,x1-x0,y1-y0);
  ctx.fillStyle='#334155'; ctx.font='12px system-ui'; ctx.fillText('Lon',(x0+x1)/2-10,y1+26);
  ctx.save(); ctx.translate(14,(y0+y1)/2+10); ctx.rotate(-Math.PI/2); ctx.fillText('Lat',0,0); ctx.restore();
  for(let i=0;i<=6;i++){ const xx=x0+(x1-x0)*i/6; ctx.beginPath(); ctx.moveTo(xx,y0); ctx.lineTo(xx,y1); ctx.strokeStyle='rgba(148,163,184,.35)'; ctx.stroke(); }
  for(let i=0;i<=6;i++){ const yy=y0+(y1-y0)*i/6; ctx.beginPath(); ctx.moveTo(x0,yy); ctx.lineTo(x1,yy); ctx.stroke(); }
  const px=Math.max(1,parseInt(document.getElementById('px').value)||6);
  const gamma=parseFloat(document.getElementById('gamma').value||1);
  for(const p of X.pts){
    const x=projX(p.lon), y=projY(p.lat);
    let t=(p.val-X.vmin)/(X.vmax-X.vmin+1e-9);
    t=Math.pow(Math.max(0,Math.min(1,t)),gamma);
    const [r,g,b]=colorViridis(t); ctx.fillStyle=`rgb(${r},${g},${b})`;
    ctx.fillRect(x-px/2,y-px/2,px,px);
  }
}
function updateXStats(){
  const vals=X.pts.map(p=>p.val).sort((a,b)=>a-b);
  const mean=vals.reduce((a,b)=>a+b,0)/vals.length;
  const q90=vals[Math.floor(vals.length*0.9)]||NaN;
  document.getElementById('xStats').innerHTML=
    `<div>点数</div><div>${vals.length}</div>
     <div>均值ppb</div><div>${mean.toFixed(1)}</div>
     <div>P90ppb</div><div>${(q90||0).toFixed?q90.toFixed(1):'—'}</div>
     <div>vmin/vmax</div><div>${X.vmin.toFixed(1)} / ${X.vmax.toFixed(1)}</div>`;
}
function exportX(){
  if(!X.pts.length){alert('请先加载数据');return;}
  const m='metric,value\npoints,'+X.pts.length+'\nmean_ppb,'+
    (X.pts.reduce((s,p)=>s+p.val,0)/X.pts.length).toFixed(2)+
    '\nvmin,'+X.vmin.toFixed(2)+'\nvmax,'+X.vmax.toFixed(2)+
    '\ngamma,'+(document.getElementById('gamma').value);
  const a=document.createElement('a'); a.href='data:text/csv;charset=utf-8,'+encodeURIComponent(m); a.download='xch4_results.csv'; a.click();
}
</script>
</html>
